import { spawn } from "child_process"
import { mkdirSync, writeFileSync } from "fs"
import { createInterface } from "readline"

// Configuration
const BUNDLE_NAME = "com.example.e2e_testing"
const START_TESTS_CMD = ["hdc", "shell", `aa start -a EntryAbility -b ${BUNDLE_NAME}`]
const STOP_TEST_CMD = ["hdc", "shell", `aa force-stop ${BUNDLE_NAME}`]
const SCREEN_ON = ["hdc", "shell", "power-shell timeout -o 864000000"]
const PRIVATE_LOG_ON_CMD = ["hdc", "shell", "hilog -p off"]
const CLEAR_LOG_CMD = ["hdc", "shell", "hilog -r"]
const GET_LOGS_CMD = ["hdc", "shell", "hilog"]
const CMD_TIMEOUT_MS = 2000
const PROGRESS_TIMEOUT = 8000
const MAX_RESTARTS = 3

// Running
main().then(() => {
    console.info("THE TESTS COMPLETED.")
    saveLogs()
    process.exit(0)
}).catch(error => {
    console.error("THE TESTS ABORTED!")
    console.error(`\n${error}`)
    saveLogs()
    process.exit(1)
})

// Ctrl+C
process.on("SIGINT", () => {
    console.warn("\nFORCED TERMINATION...")
    saveLogs()
    process.exit(0)
})

async function main() {
    await runCommand(SCREEN_ON)
    await runCommand(PRIVATE_LOG_ON_CMD)
    await testControl()
}

const report = []
const logs = []
function saveLogs() {
    const folder = "result"
    mkdirSync("result", { recursive: true })
    writeFileSync(`${folder}/report.txt`, report.join("\n"))
    console.info(`${folder}/report.txt: the tests log only.`)
    writeFileSync(`${folder}/test_log.txt`, logs.join("\n"))
    console.info(`${folder}/test_log.txt: full log.`)
}

async function runCommand(cmd /*string[]*/, timeout = CMD_TIMEOUT_MS) {
    return new Promise((resolve, reject) => {
        const child = spawn(cmd[0], cmd.slice(1))
        const timeoutId = setTimeout(() => {
            if (child.pid) {
                child.kill()
            }
            reject(new Error(`CMD timeout cmd: ${cmd}`))
        }, timeout)
        child.on("close", () => {
            clearTimeout(timeoutId)
            resolve()
        })
        child.on("error", err => {
            clearTimeout(timeoutId)
            reject(err)
        })
    })
}

async function testControl() {
    return new Promise(async (resolve, reject) => {
        let controlId = 0
        let logTimeoutMs = PROGRESS_TIMEOUT
        let child /*ChildProcessWithoutNullStreams|undefined*/ = undefined
        let timeoutId /*number|undefined*/ = undefined
        let tries = 0
        let lastTest = -1
        const progress /*Progress*/ = { run: 0 }
        const startTime = Date.now()

        // Running and monitoring tests by timer.
        const control = async (id) => {
            if (id !== controlId) return
            if (child?.pid) {
                console.warn("TIMEOUT!")
                child.kill()
            }

            // Sending commands to restart testing. Start monitoring.
            const startTest = async () => {
                try {
                    await runCommand(STOP_TEST_CMD)
                    await runCommand(CLEAR_LOG_CMD)
                    child = spawn(GET_LOGS_CMD[0], GET_LOGS_CMD.slice(1))

                    const refreshTests = () => {
                        clearTimeout(timeoutId)
                        timeoutId = setTimeout(control, logTimeoutMs, ++controlId)
                    }
                    const finishTests = () => {
                        clearTimeout(timeoutId)
                        const timeSpent = `Time spent: ${formatMsToMMSS(Date.now() - startTime)}`
                        console.info(timeSpent)
                        report.push(timeSpent)
                        resolve()
                    }
                    child.stdout.on("data", data => readTestLog(data.toString(), progress, refreshTests, finishTests))
                    await runCommand(restoreTestCmd(START_TESTS_CMD, progress))
                    timeoutId = setTimeout(control, logTimeoutMs, ++controlId)
                } catch (e) {
                    control(++controlId)
                }
            }

            // The first start tests. Or restart tests if the last test index was changed.
            if (progress.run !== lastTest) {
                lastTest = progress.run
                tries = 0
                startTest()
            // There seems to be no connection to the device. Let's try it a few times.
            } else if (tries < MAX_RESTARTS) {
                ++tries
                startTest()
            // There's no point in trying anymore.
            } else {
                const userAction = await askUserAction()
                switch (userAction) {
                    case 2:
                        logTimeoutMs += 1000
                    case 1: {
                        tries = 0
                        await runCommand(SCREEN_ON)
                        await runCommand(PRIVATE_LOG_ON_CMD)
                        startTest()
                        break
                    }
                    default: {
                        reject(new Error("The connection is lost"))
                        break
                    }
                }
            }
        }
        // The first start.
        control(controlId)
    })
}

function readTestLog(data /*string*/, progress /*object*/, refreshTests /*()=>void*/, finishTests /*()=>void*/) {
    const newLogs = data.split(/\r?\n/)
    const newTestLines = []
    for (const line of newLogs) {
        if (line) {
            logs.push(line)
            const tagIndex = line.indexOf("[TEST]")
            if (tagIndex >= 0) {
                const message = line.substring(tagIndex + 7)
                newTestLines.push(message)
                if (!message.includes("[PROGRESS]")) {
                    report.push(message)
                    console.info(message)
                }
            }
        }
    }
    if (/Ran: \d+, Passed: \d+, Failed: \d+, Errors: \d+, Skipped: \d+/.test(data)) {
        finishTests()
    } else {
        const newProgress = getTestProgress(newTestLines)
        if (newProgress.run > progress.run) {
            Object.assign(progress, newProgress)
            refreshTests()
        }
    }
}

function getTestProgress(logs /*string[]*/) /*Progress*/ {
    let result = { run: -1 }
    for (const log of logs) {
        const match = /run: (\d+), suite: (\d+), test: (\d+), passed: (\d+), failed: (\d+), errors: (\d+)/.exec(log)
        if (match?.length >= 6) {
            result = {
                run: parseInt(match[1]), suite: parseInt(match[2]), test: parseInt(match[3]),
                passed: parseInt(match[4]), failed: parseInt(match[5]), errors: parseInt(match[6]),
            }
        }
    }
    return result
}

function restoreTestCmd(cmd /*string[]*/, progress /*Progress*/) /*string[]*/ {
    return (progress.suite !== undefined)
        ? [...cmd.slice(0, -1),
            cmd.at(-1) + ` -A restore --pi run ${progress.run} --pi suite ${progress.suite} --pi test ${progress.test} `
            + `--pi passed ${progress.passed} --pi failed ${progress.failed} --pi errors ${progress.errors}`]
        : cmd
}

const QUESTION = `
**********************
The connection is lost
1. try again
2. try again (increase the time limit)
3. stop and save logs
Select action: `

function askUserAction() /*Promise<number>*/ {
    return new Promise((resolve, reject) => {
        const rl = createInterface({
            input: process.stdin,
            output: process.stdout
        })
        rl.question(QUESTION, (answer) => {
            rl.close()
            resolve(+answer.trim())
        })
    })
}

function formatMsToMMSS(ms /*number*/) /*string*/ {
  const totalSeconds = Math.round(ms / 1000)
  const minutes = Math.floor(totalSeconds / 60)
  const seconds = totalSeconds % 60
  const formattedMinutes = String(minutes).padStart(2, '0')
  const formattedSeconds = String(seconds).padStart(2, '0')
  return `${formattedMinutes} min ${formattedSeconds} sec`
}

/*interface Progress {
    run: number
    suite?: number
    test?: number
    passed?: number
    failed?: number
    errors?: number
}*/
